#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the version 3 of the GNU Lesser General Public License
#   as published by the Free Software Foundation.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU Lesser General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Copyright (c) NEC Deutschland GmbH, NEC HPC Europe
#
# $Id$

import logging
import io
from aggmon.aggregator import Aggregator, AggregatorComponent
from aggmon.dp_aggregator import DPAggregator, DPAggregatorComponent
from aggmon.importer import Importer, ImporterComponent
from aggmon.databases.metric.metric_database import MetricDatabaseComponent
from aggmon.databases.metric.metric_store import MetricStore
from aggregators import *
from importers import *
from databases.metric.metric_stores import *
import ConfigParser
from shlex import shlex


# global variables
preset_aggregators = {}


def escape_arg( arg, escape=False ):
    if arg.startswith( "<" ) and arg.endswith( ">" ):
        arg = arg.lstrip( "<" ).rstrip( ">" )
    else:
        if escape:
            arg = "\"" + arg + "\""
    return arg


def morph_args( line, whitespace=":", escape=False ):
    """
    """
    splitter = shlex( line, posix=True )
    splitter.whitespace = whitespace
    splitter.whitespace_split = True
    args = list( splitter )

    # check for key=value format and escape value, if needed
    new_args = []
    for arg in args:
        arr = arg.split( "=", 1 )
        if len( arr ) == 1:
            new_args.append( escape_arg( arr[0], escape ) )
        else:
            new_args.append( arr[0] + "=" + escape_arg( arr[1], escape ) )
    return new_args


def morph_args_kwds( line, whitespace=":", escape=False ):
    """
    """
    splitter = shlex( line, posix=True )
    splitter.whitespace = whitespace
    splitter.whitespace_split = True
    args = list( splitter )

    # check for key=value format and escape value, if needed
    new_args = []
    new_kwds = {}
    for arg in args:
        arr = arg.split( "=", 1 )
        if len( arr ) == 1:
            try:
                val = eval( escape_arg( arr[0], escape ) )
            except:
                val = escape_arg( arr[0], escape )
            new_args.append( val )
        else:
            try:
                val = eval( escape_arg( arr[1], escape ) )
            except:
                val = escape_arg( arr[1], escape )
            new_kwds[arr[0]] = val
    return new_args, new_kwds


def parse_config( file_name=None, config_string=None ):
    """
    Read a config file or a config string and return the corresponding configparser object.
    """
    config = ConfigParser.ConfigParser()
    config.optionxform = str    # keep options case sensitive (default is to make them lower-case)
    if file_name is not None:
        try:
            fp = open( file_name )
        except:
            logging.error( "trying to open config file '%s'" % file_name )
            return None
        config.readfp( fp )
    elif config_string is not None:
        try:
            config.readfp( io.BytesIO( config_string ) )
        except:
            logging.error( "parsing config string" )
            return None
    else:
        logging.error( "neither config file nor config string passed!?!?" )
    return config


def parse_aggregator_config( components, file_name=None, config_string=None, config_obj=None, group_path=None ):
    """
    Read either from an aggregator config file, a configuration string or a
    configparser object and create the corresponding aggregator instances.
    """
    if config_obj is None:
        config = parse_config( file_name=file_name, config_string=config_string )
    else:
        config = config_obj

    assert isinstance( config, ConfigParser.ConfigParser ), "config must be a ConfigParser object!"

    preset_agg = parse_preset_aggregators( config )

    agg = {}
    if config.has_section( "aggregate" ):
        for key, value in config.items( "aggregate" ):
            # configuration line has the form:
            # <metric_name> as <agg_metric_name> = <aggregator_class>[:parameter1=value1[:...]]
            metric, agg_metric = key.split( " as " )
            if len( metric ) == 0 or len( agg_metric ) == 0:
                continue
            __args, __kwds = morph_args_kwds( value )

            class_name = __args[0] + "Aggregator"

            pagg = None
            # check that class exists
            if class_name in globals():
                if issubclass( globals()[class_name], Aggregator ):
                    logging.debug( "Found Aggregator class '%s', will instantiate it for metric '%s'" % (class_name, agg_metric) )
                elif issubclass( globals()[class_name], DPAggregator ):
                    logging.debug( "Found DPAggregator class '%s', will instantiate it for metric '%s'" % (class_name, agg_metric) )
                else:
                    logging.error( "Class '%s' is not an Aggregator, skipping agg_metric '%s'" % (class_name, agg_metric) )
                    continue
            else:
                # must be a preset aggregator
                class_name = class_name.rstrip( "Aggregator" )
                if class_name in preset_agg:
                    pagg = preset_agg[class_name]
                    logging.debug( "Found preset Aggregator '%s' (%s), will instantiate it for metric '%s'" %
                                   (class_name, pagg["base_class"], agg_metric) )
                    class_name = pagg["base_class"]
                    for k, v in pagg.items():
                        if k == "base_class":
                            continue
                        __kwds[k] = v
                else:
                    logging.error( "Aggregator '%s' not found, skipping agg_metric '%s'." % (class_name, agg_metric) )
                    continue

            if agg_metric in agg:
                logging.warning( "attempting redefinition of aggregated metric '%s', ignoring it." % agg_metric )
                continue
            agg[agg_metric] = 1

            component_name = class_name + "Component"
            __id = class_name + "_" + agg_metric
            __args = [ "%s" % metric, "%s" % agg_metric, "%s" % group_path ] + __args[1:]
            try:
                a = None
                exec( "a=%s(name=__id, cmd=class_name, args=__args, kwds=__kwds, requires_re=['Hierarchy', 'Channel'])" % component_name )
                components.add( a )
            except Exception, e:
                logging.error( "instantiating %s failed. Exception: %s" % (component_name, str(e)) )

        config.remove_section( "aggregate" )


def parse_database_config( components, file_name=None, config_string=None, config_obj=None ):
    if config_obj is None:
        config = parse_config( file_name=file_name, config_string=config_string )

    assert isinstance( config, ConfigParser.ConfigParser ), "config must be a ConfigParser object!"

    if config.has_section( "databases" ):
        for key, value in config.items( "databases" ):
            splitted = key.split( " " )
            store_class_name = splitted[0]
            if len( store_class_name ) == 0:
                continue
            __args, __kwds = morph_args_kwds( value, escape=True )

            store_class_name = store_class_name + "MetricStore"

            # check that class exists
            if store_class_name in globals():
                if issubclass( globals()[store_class_name], MetricStore ):
                    logging.debug( "Found MetricStore class '%s', instantiate MetricDatabase for it." % store_class_name )
                else:
                    logging.error( "Class '%s' is not a MetricStore, skipping..." % store_class_name )
                    continue
            else:
                logging.error( "MetricStore class '%s' not found, skipping it." % store_class_name )
                continue

            __kwds["store"] = store_class_name
            __id = "_".join( splitted )
            try:
                a = None
                exec( "a=MetricDatabaseComponent(name='MetricDatabase_'+__id, args=__args, kwds=__kwds, requires_re=['Hierarchy', 'GroupChannel'] )" )
                components.add( a )
            except Exception, e:
                logging.error( "instantiating MetricdatabaseComponent failed. Exception: %s" % str( e ) )
        config.remove_section( "databases" )


def parse_importer_config( components, file_name=None, config_string=None, config_obj=None ):
    """
    Read either from an importer config file, a configuration string or a
    configparser object and create the corresponding importer instances.

    The configuration section is called "importers".
    Each configuration line has the format:
    <importer_base_name>[ <id>] = parameter1=value1[:...] 

    Configuration example:
    [importers]
    GangliaXMLMetric 1 = host_name="192.168.50.132":only_group=<True>:only_self=<True>
    NagiosStatusLog = url="ssh://localhost/var/log/nagios/status.log"

    Rules:
    Protect strings containing ":" from splitting by enclosing them in quotes.
    Arguments starting with "<" and ending with ">" are used without string quoting!
    Use <...> for numbers, booleans, variables.
    """
    if config_obj is None:
        config = parse_config( file_name=file_name, config_string=config_string )

    assert isinstance( config, ConfigParser.ConfigParser ), "config must be a ConfigParser object!"

    if config.has_section( "importers" ):
        for key, value in config.items( "importers" ):
            splitted = key.split( " " )
            importer_class_name = splitted[0]
            if len( importer_class_name ) == 0:
                continue
            __args, __kwds = morph_args_kwds( value, escape=True )

            importer_class_name = importer_class_name + "Importer"

            # check that class exists
            if importer_class_name in globals():
                if issubclass( globals()[importer_class_name], Importer ):
                    logging.debug( "Found Importer class '%s', will instantiate it." % importer_class_name )
                else:
                    logging.error( "Class '%s' is not an Importer, skipping..." % importer_class_name )
                    continue
            else:
                logging.error( "Importer class '%s' not found, skipping it." % importer_class_name )
                continue

            component_name = importer_class_name + "Component"
            __id = "_".join( splitted )
            try:
                a = None
                exec( "a=%s(name=__id, cmd=importer_class_name, args=__args, kwds=__kwds, requires_re=['Hierarchy', 'Channel'])" % component_name )
                components.add( a )
            except Exception, e:
                logging.error( "instantiating %s failed. Exception: %s" % (component_name, str(e)) )
        config.remove_section( "importers" )


def parse_preset_aggregators( config ):
    """
    Parse preset aggregator definitions in the configuration file.
    """
    assert isinstance( config, ConfigParser.ConfigParser ), "config must be a ConfigParser object!"

    paggs = {}
    for section in config.sections():
        if section.startswith( "aggregator_preset " ):
            items = {}
            for k,v in config.items( section ):
                items[k] = v
            new_agg = section.lstrip( "aggregator_preset " )
            if new_agg in paggs:
                logging.warning( "redefining preset aggregator '%s': ignored." % new_agg )
                continue
            # Attention: config parser lowers the left side!
            if not "base_class" in items:
                logging.warning( "'base_class' not defined for preset aggregator '%s', ignoring it." % new_agg )
                continue
            paggs[new_agg] = items
    return paggs
